/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package sirtet.netplay.server;

import java.util.Collection;
import sirtet.event.BoardActionEvent;
import sirtet.event.BoardBlocksEliminatedEvent;
import sirtet.event.BoardEvent;
import sirtet.event.BoardPieceEvent;
import sirtet.event.BoardPieceFixedEvent;
import sirtet.netplay.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import sirtet.ControlledPlayer;
import sirtet.GameAction;
import sirtet.GameBoard;
import sirtet.Piece;
import sirtet.PieceProvider;
import sirtet.event.BoardListener;
import sirtet.event.PlayerActionEvent;
import sirtet.event.PlayerEvent;
import sirtet.event.PlayerListener;
import sirtet.tetris.TetrisRoom;

/**
 * A collection of GameBoards as well as participating players
 * so that they communicate (e.g. compete) with each other synchronously.
 * @author guangwei.zhu
 */
public abstract class GameRoom {

    protected Map<NetworkPlayer, GameBoard> seatTable = new HashMap<NetworkPlayer, GameBoard>();
    private int maxPlayers = 2;
    private int maxBoards = 2;
    private int minPlayers = 2;
    private int minBoards = 2;
    private String typeString;
    private boolean playing = false;
    private PlayerListener listener = new PlayerListener() {

        public void actionPerformed(PlayerActionEvent evt) {
        }

        public void playerQuit(PlayerEvent evt) {
            GameRoom.this.playerQuit((NetworkPlayer) evt.getSource());
        }

        public void playerEnabled(PlayerEvent evt) {
        }

        public void playerDisabled(PlayerEvent evt) {
        }

        public void playerReady(PlayerEvent evt) {
            if (canStart()) {
                start();
            }
        }
    };

    /**
     * A class that relays new piece information to the remote clients.
     */
    protected class SynchronousPieceProvider extends PieceProvider {

        private NetworkPlayer player;
        private PieceProvider provider;

        public SynchronousPieceProvider(PieceProvider provider, NetworkPlayer player) {
            this.player = player;
            this.provider = provider;
        }

        public void dispose() {
            this.player = null;
            this.provider = null;
        }

        @Override
        public Piece requestPiece() {
            Piece piece = provider.requestPiece();
            try {
                broadcast(new GameAction(player.id(), GameBoard.MSG_PIECE + piece.toString()));
            } catch (Exception ex) {
                dispose();
            }

            return piece;
        }

        @Override
        public int remaining() {
            return provider.remaining();
        }
    }

    /**
     * Send a message string to all participating players.
     * @param message a message string to be sent to all participants.
     */
    protected void broadcast(String message) {
        // Broadcast to all players
        for (NetworkPlayer player : getPlayers()) {
            player.getCommunicator().post(message);
        }

    }

    /**
     * Send an action to all participating players.
     * @param action an action (either anonymous or specific) to be sent to all participants.
     */
    protected void broadcast(GameAction action) {
        // Broadcast to all players
        for (NetworkPlayer player : getPlayers()) {
            player.getCommunicator().post(action.toString());
        }

    }

    /**
     * Send a message string to all but one excepted player.
     * @param message a message string to be sent.
     * @param except a player to whom the message will not be sent
     */
    protected void broadcastExcept(String message, NetworkPlayer except) {
        // Broadcast to all players
        for (NetworkPlayer player : getPlayers()) {
            if (!player.equals(except)) {   // Except the specified player
                player.getCommunicator().post(message);
            }
        }

    }

    /**
     * Send a message string to all but several excepted players.
     * @param message a message string to be sent.
     * @param except a collection of players to whom the message will not be sent
     */
    protected void broadcastExcept(String message, Collection<NetworkPlayer> except) {
        // Broadcast to all players
        for (NetworkPlayer player : getPlayers()) {
            if (!except.contains(player)) {   // Except the specified players
                player.getCommunicator().post(message);
            }
        }

    }

    private class ActionBroadcaster implements BoardListener {

        private NetworkPlayer individual;

        public ActionBroadcaster(NetworkPlayer player) {
            this.individual = player;
        }

        public void boardChanged(BoardEvent evt) {
        }

        public void boardGameOver(BoardEvent evt) {
        }

        public void boardStarted(BoardEvent evt) {
            // Send individually
            individual.getCommunicator().post(ServerResponse.START);
        }

        public void boardReset(BoardEvent evt) {
        }

        public void boardPieceFixed(BoardPieceFixedEvent evt) {
        }

        public void boardNextPieceChanged(BoardPieceEvent evt) {
        }

        public void boardBlocksEliminated(BoardBlocksEliminatedEvent evt) {
        }

        public void boardActionTriggered(BoardActionEvent evt) {
            broadcast(evt.action.toString());
        }

        public void boardDisposing(BoardEvent evt) {
            // Remove all players related to the board being disposed
            Set<NetworkPlayer> unavailPlayers = new HashSet<NetworkPlayer>();
            for (NetworkPlayer nPlayer : seatTable.keySet()) {
                if (seatTable.get(nPlayer) == evt.getSource()) {
                    unavailPlayers.add(nPlayer);
                }
            }
            for (NetworkPlayer nPlayer : unavailPlayers) {
                unregisterPlayer(nPlayer);
            }
        }
    }

    /**
     * Construct a concrete instance of a GameBoard
     * @param  individual the NetworkPlayer who is requesting a new room
     * @return the new GameBoard
     */
    protected abstract GameBoard createBoard(NetworkPlayer player);

    /**
     * Protected constructor, only factories can create instance
     */
    protected GameRoom() {
    }

    /**
     * Get the set of all GameBoard objects in this room
     * @return the set of all GameBoard objects
     */
    protected synchronized Set<GameBoard> getBoards() {
        Set<GameBoard> boards = new HashSet<GameBoard>();
        boards.addAll(seatTable.values());
        return boards;
    }

    /**
     * Get the set of all NetworkPlayer objects in this room
     * @return the set of all NetworkPlayer objects
     */
    protected synchronized Set<NetworkPlayer> getPlayers() {
        return seatTable.keySet();
    }

    /**
     * Return the maximum number of boards allowed in this room.
     * @return the maximum allowed number of boards
     */
    public int getMaxBoards() {
        return maxBoards;
    }

    /**
     * Set the maximum number of boards allowed in this room.
     * @param maxBoards the maximum allowed number of boards
     */
    protected void setMaxBoards(int maxBoards) {
        this.maxBoards = maxBoards;
    }

    /**
     * Return the maximum number of players allowed in this room.
     * @return the maximum allowed number of players
     */
    public int getMaxPlayers() {
        return maxPlayers;
    }

    /**
     * Set the maximum number of players allowed in this room.
     * @param maxPlayers the maximum allowed number of players
     */
    protected void setMaxPlayers(int maxPlayers) {
        this.maxPlayers = maxPlayers;
    }

    /**
     * Return the minimum number of boards allowed in this room.
     * @return the minimum allowed number of boards
     */
    public int getMinBoards() {
        return minBoards;
    }

    /**
     * Set the minimum number of boards allowed in this room.
     * @param minBoards the minimum allowed number of boards
     */
    protected void setMinBoards(int minBoards) {
        this.minBoards = minBoards;
    }

    /**
     * Return the minimum number of players allowed in this room.
     * @return the minimum allowed number of players
     */
    public int getMinPlayers() {
        return minPlayers;
    }

    /**
     * Set the minimum number of players allowed in this room.
     * @param minPlayers the minimum allowed number of players
     */
    protected void setMinPlayers(int minPlayers) {
        this.minPlayers = minPlayers;
    }

    /**
     * Return the string that indicates the GameType
     * @return the gametype string (e.g. Tetris for TetrisPlayer)
     */
    public String gameType() {
        return typeString;
    }

    /**
     * Set the string that indicates the GameType
     * @param typeString the gametype string (e.g. Tetris for TetrisPlayer)
     */
    protected void setGameType(String typeString) {
        this.typeString = typeString;
    }

    /**
     * Handles the event when a request of quitting is received.  By default
     * the handler will remove its relation from the table.
     * @param individual the individual requesting to quit
     */
    protected void playerQuit(NetworkPlayer player) {
        GameBoard board = seatTable.get(player);
        if (board != null) {
            unregisterPlayer(player);
        }
    }

    /**
     * Starts the current game.  Once started, the GameRoom can accept new players
     * no longer.
     * @return true if the game is successfully started
     */
    public synchronized boolean start() {
        // Check condition
        if (canStart()) {
            this.playing = true;
            for (GameBoard board : this.getBoards()) {
                board.start();
            }
            for (NetworkPlayer player : this.getPlayers()) {
                player.getCommunicator().post(ServerResponse.START);
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Check the start condition, see if the game can be started
     * @return true if the game can be started at this moment
     */
    public synchronized boolean canStart() {
        int numPlayers = this.getPlayers().size();
        int numBoards = this.getBoards().size();

        if (numPlayers > this.getMaxPlayers() || numPlayers < this.getMinPlayers()) {
            return false;
        }
        if (numBoards > this.getMaxBoards() || numBoards < this.getMinBoards()) {
            return false;
        }

        // If all players are ready
        for (NetworkPlayer player : this.getPlayers()) {
            if (!player.ready()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Register the player in this room with its participating game board.
     */
    protected void registerPlayer(NetworkPlayer player, GameBoard board) {
        if (!seatTable.containsKey(player)) {
            // Broadcast information
            broadcast(ServerResponse.JOIN + " " + player.id());

            for (NetworkPlayer nplayer : getPlayers()) {
                player.getCommunicator().post(ServerResponse.JOIN + " " + nplayer.id());
            }

            seatTable.put(player, board);
        }
    }

    /**
     * Unregister the player in this room with its participating game board.
     */
    protected void unregisterPlayer(NetworkPlayer player) {
        if (seatTable.containsKey(player)) {
            seatTable.remove(player);

            // Broadcast information
            broadcast(ServerResponse.QUIT + " " + player.id());
        }
    }

    /**
     * Participate in this GameRoom
     * @param individual the participant
     * @return true if the individual is accepted; otherwise false
     */
    public synchronized boolean participate(NetworkPlayer player) {
        Set<GameBoard> boards;
        Set<GameBoard> acceptableBoards;
        if (this.playing) {
            return false;       // Game already started, it's too late
            }            // Check individual number
        if (seatTable.size() >= this.getMaxPlayers()) {
            return false;   // Too many players
            }
        boards = this.getBoards();
        acceptableBoards = new HashSet<GameBoard>();
        // Get acceptable boards
        for (GameBoard board : boards) {
            if (board.admitsNewPlayer(player)) {
                acceptableBoards.add(board);
            }
        }

        GameBoard boardToEnter;
        // If there is a vacant room for the individual already
        if (acceptableBoards.size() > 0) {
            boardToEnter = player.choose(acceptableBoards);
        } else {
            // Check board number
            if (boards.size() < this.getMaxBoards()) {
                boardToEnter = this.createBoard(player);
            } else {
                return false;
            }
        }
        // If admissible
        if (boardToEnter != null && boardToEnter.admitsNewPlayer(player)) {
            registerPlayer(player, boardToEnter);
            player.addPlayerListener(this.listener);
            if (this instanceof TetrisRoom) {
                boardToEnter.registerPlayer(new ControlledPlayer(player, 20));
            } else {
                boardToEnter.registerPlayer(player);
            }
            boardToEnter.addBoardListener(new ActionBroadcaster(player));

            // Ask the individual if ready
            player.getCommunicator().post(ServerResponse.ASK_READY);
            return true;
        } else {
            return false;
        }
    }
}
